<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Controller
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Abstract.php 23967 2011-05-03 14:31:55Z adamlundrigan $
 */

/**
 * Zend_Controller_Response_Abstract
 *
 * Base class for Zend_Controller responses
 *
 * @package Zend_Controller
 * @subpackage Response
 * @copyright  Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Controller_Response_Abstract {
	/**
	 * Body content
	 * @var array
	 */
	protected $_body = array ();
	
	/**
	 * Exception stack
	 * @var Exception
	 */
	protected $_exceptions = array ();
	
	/**
	 * Array of headers. Each header is an array with keys 'name' and 'value'
	 * @var array
	 */
	protected $_headers = array ();
	
	/**
	 * Array of raw headers. Each header is a single string, the entire header to emit
	 * @var array
	 */
	protected $_headersRaw = array ();
	
	/**
	 * HTTP response code to use in headers
	 * @var int
	 */
	protected $_httpResponseCode = 200;
	
	/**
	 * Flag; is this response a redirect?
	 * @var boolean
	 */
	protected $_isRedirect = false;
	
	/**
	 * Whether or not to render exceptions; off by default
	 * @var boolean
	 */
	protected $_renderExceptions = false;
	
	/**
	 * Flag; if true, when header operations are called after headers have been
	 * sent, an exception will be raised; otherwise, processing will continue
	 * as normal. Defaults to true.
	 *
	 * @see canSendHeaders()
	 * @var boolean
	 */
	public $headersSentThrowsException = true;
	
	/**
	 * Normalize a header name
	 *
	 * Normalizes a header name to X-Capitalized-Names
	 *
	 * @param  string $name
	 * @return string
	 */
	protected function _normalizeHeader($name) {
		$filtered = str_replace ( array ('-', '_' ), ' ', ( string ) $name );
		$filtered = ucwords ( strtolower ( $filtered ) );
		$filtered = str_replace ( ' ', '-', $filtered );
		return $filtered;
	}
	
	/**
	 * Set a header
	 *
	 * If $replace is true, replaces any headers already defined with that
	 * $name.
	 *
	 * @param string $name
	 * @param string $value
	 * @param boolean $replace
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setHeader($name, $value, $replace = false) {
		$this->canSendHeaders ( true );
		$name = $this->_normalizeHeader ( $name );
		$value = ( string ) $value;
		
		if ($replace) {
			foreach ( $this->_headers as $key => $header ) {
				if ($name == $header ['name']) {
					unset ( $this->_headers [$key] );
				}
			}
		}
		
		$this->_headers [] = array ('name' => $name, 'value' => $value, 'replace' => $replace );
		
		return $this;
	}
	
	/**
	 * Set redirect URL
	 *
	 * Sets Location header and response code. Forces replacement of any prior
	 * redirects.
	 *
	 * @param string $url
	 * @param int $code
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setRedirect($url, $code = 302) {
		$this->canSendHeaders ( true );
		$this->setHeader ( 'Location', $url, true )->setHttpResponseCode ( $code );
		
		return $this;
	}
	
	/**
	 * Is this a redirect?
	 *
	 * @return boolean
	 */
	public function isRedirect() {
		return $this->_isRedirect;
	}
	
	/**
	 * Return array of headers; see {@link $_headers} for format
	 *
	 * @return array
	 */
	public function getHeaders() {
		return $this->_headers;
	}
	
	/**
	 * Clear headers
	 *
	 * @return Zend_Controller_Response_Abstract
	 */
	public function clearHeaders() {
		$this->_headers = array ();
		
		return $this;
	}
	
	/**
	 * Clears the specified HTTP header
	 *
	 * @param  string $name
	 * @return Zend_Controller_Response_Abstract
	 */
	public function clearHeader($name) {
		if (! count ( $this->_headers )) {
			return $this;
		}
		
		foreach ( $this->_headers as $index => $header ) {
			if ($name == $header ['name']) {
				unset ( $this->_headers [$index] );
			}
		}
		
		return $this;
	}
	
	/**
	 * Set raw HTTP header
	 *
	 * Allows setting non key => value headers, such as status codes
	 *
	 * @param string $value
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setRawHeader($value) {
		$this->canSendHeaders ( true );
		if ('Location' == substr ( $value, 0, 8 )) {
			$this->_isRedirect = true;
		}
		$this->_headersRaw [] = ( string ) $value;
		return $this;
	}
	
	/**
	 * Retrieve all {@link setRawHeader() raw HTTP headers}
	 *
	 * @return array
	 */
	public function getRawHeaders() {
		return $this->_headersRaw;
	}
	
	/**
	 * Clear all {@link setRawHeader() raw HTTP headers}
	 *
	 * @return Zend_Controller_Response_Abstract
	 */
	public function clearRawHeaders() {
		$this->_headersRaw = array ();
		return $this;
	}
	
	/**
	 * Clears the specified raw HTTP header
	 *
	 * @param  string $headerRaw
	 * @return Zend_Controller_Response_Abstract
	 */
	public function clearRawHeader($headerRaw) {
		if (! count ( $this->_headersRaw )) {
			return $this;
		}
		
		$key = array_search ( $headerRaw, $this->_headersRaw );
		if ($key !== false) {
			unset ( $this->_headersRaw [$key] );
		}
		
		return $this;
	}
	
	/**
	 * Clear all headers, normal and raw
	 *
	 * @return Zend_Controller_Response_Abstract
	 */
	public function clearAllHeaders() {
		return $this->clearHeaders ()->clearRawHeaders ();
	}
	
	/**
	 * Set HTTP response code to use with headers
	 *
	 * @param int $code
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setHttpResponseCode($code) {
		if (! is_int ( $code ) || (100 > $code) || (599 < $code)) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Invalid HTTP response code' );
		}
		
		if ((300 <= $code) && (307 >= $code)) {
			$this->_isRedirect = true;
		} else {
			$this->_isRedirect = false;
		}
		
		$this->_httpResponseCode = $code;
		return $this;
	}
	
	/**
	 * Retrieve HTTP response code
	 *
	 * @return int
	 */
	public function getHttpResponseCode() {
		return $this->_httpResponseCode;
	}
	
	/**
	 * Can we send headers?
	 *
	 * @param boolean $throw Whether or not to throw an exception if headers have been sent; defaults to false
	 * @return boolean
	 * @throws Zend_Controller_Response_Exception
	 */
	public function canSendHeaders($throw = false) {
		$ok = headers_sent ( $file, $line );
		if ($ok && $throw && $this->headersSentThrowsException) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Cannot send headers; headers already sent in ' . $file . ', line ' . $line );
		}
		
		return ! $ok;
	}
	
	/**
	 * Send all headers
	 *
	 * Sends any headers specified. If an {@link setHttpResponseCode() HTTP response code}
	 * has been specified, it is sent with the first header.
	 *
	 * @return Zend_Controller_Response_Abstract
	 */
	public function sendHeaders() {
		// Only check if we can send headers if we have headers to send
		if (count ( $this->_headersRaw ) || count ( $this->_headers ) || (200 != $this->_httpResponseCode)) {
			$this->canSendHeaders ( true );
		} elseif (200 == $this->_httpResponseCode) {
			// Haven't changed the response code, and we have no headers
			return $this;
		}
		
		$httpCodeSent = false;
		
		foreach ( $this->_headersRaw as $header ) {
			if (! $httpCodeSent && $this->_httpResponseCode) {
				header ( $header, true, $this->_httpResponseCode );
				$httpCodeSent = true;
			} else {
				header ( $header );
			}
		}
		
		foreach ( $this->_headers as $header ) {
			if (! $httpCodeSent && $this->_httpResponseCode) {
				header ( $header ['name'] . ': ' . $header ['value'], $header ['replace'], $this->_httpResponseCode );
				$httpCodeSent = true;
			} else {
				header ( $header ['name'] . ': ' . $header ['value'], $header ['replace'] );
			}
		}
		
		if (! $httpCodeSent) {
			header ( 'HTTP/1.1 ' . $this->_httpResponseCode );
			$httpCodeSent = true;
		}
		
		return $this;
	}
	
	/**
	 * Set body content
	 *
	 * If $name is not passed, or is not a string, resets the entire body and
	 * sets the 'default' key to $content.
	 *
	 * If $name is a string, sets the named segment in the body array to
	 * $content.
	 *
	 * @param string $content
	 * @param null|string $name
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setBody($content, $name = null) {
		if ((null === $name) || ! is_string ( $name )) {
			$this->_body = array ('default' => ( string ) $content );
		} else {
			$this->_body [$name] = ( string ) $content;
		}
		
		return $this;
	}
	
	/**
	 * Append content to the body content
	 *
	 * @param string $content
	 * @param null|string $name
	 * @return Zend_Controller_Response_Abstract
	 */
	public function appendBody($content, $name = null) {
		if ((null === $name) || ! is_string ( $name )) {
			if (isset ( $this->_body ['default'] )) {
				$this->_body ['default'] .= ( string ) $content;
			} else {
				return $this->append ( 'default', $content );
			}
		} elseif (isset ( $this->_body [$name] )) {
			$this->_body [$name] .= ( string ) $content;
		} else {
			return $this->append ( $name, $content );
		}
		
		return $this;
	}
	
	/**
	 * Clear body array
	 *
	 * With no arguments, clears the entire body array. Given a $name, clears
	 * just that named segment; if no segment matching $name exists, returns
	 * false to indicate an error.
	 *
	 * @param  string $name Named segment to clear
	 * @return boolean
	 */
	public function clearBody($name = null) {
		if (null !== $name) {
			$name = ( string ) $name;
			if (isset ( $this->_body [$name] )) {
				unset ( $this->_body [$name] );
				return true;
			}
			
			return false;
		}
		
		$this->_body = array ();
		return true;
	}
	
	/**
	 * Return the body content
	 *
	 * If $spec is false, returns the concatenated values of the body content
	 * array. If $spec is boolean true, returns the body content array. If
	 * $spec is a string and matches a named segment, returns the contents of
	 * that segment; otherwise, returns null.
	 *
	 * @param boolean $spec
	 * @return string|array|null
	 */
	public function getBody($spec = false) {
		if (false === $spec) {
			ob_start ();
			$this->outputBody ();
			return ob_get_clean ();
		} elseif (true === $spec) {
			return $this->_body;
		} elseif (is_string ( $spec ) && isset ( $this->_body [$spec] )) {
			return $this->_body [$spec];
		}
		
		return null;
	}
	
	/**
	 * Append a named body segment to the body content array
	 *
	 * If segment already exists, replaces with $content and places at end of
	 * array.
	 *
	 * @param string $name
	 * @param string $content
	 * @return Zend_Controller_Response_Abstract
	 */
	public function append($name, $content) {
		if (! is_string ( $name )) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Invalid body segment key ("' . gettype ( $name ) . '")' );
		}
		
		if (isset ( $this->_body [$name] )) {
			unset ( $this->_body [$name] );
		}
		$this->_body [$name] = ( string ) $content;
		return $this;
	}
	
	/**
	 * Prepend a named body segment to the body content array
	 *
	 * If segment already exists, replaces with $content and places at top of
	 * array.
	 *
	 * @param string $name
	 * @param string $content
	 * @return void
	 */
	public function prepend($name, $content) {
		if (! is_string ( $name )) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Invalid body segment key ("' . gettype ( $name ) . '")' );
		}
		
		if (isset ( $this->_body [$name] )) {
			unset ( $this->_body [$name] );
		}
		
		$new = array ($name => ( string ) $content );
		$this->_body = $new + $this->_body;
		
		return $this;
	}
	
	/**
	 * Insert a named segment into the body content array
	 *
	 * @param  string $name
	 * @param  string $content
	 * @param  string $parent
	 * @param  boolean $before Whether to insert the new segment before or
	 * after the parent. Defaults to false (after)
	 * @return Zend_Controller_Response_Abstract
	 */
	public function insert($name, $content, $parent = null, $before = false) {
		if (! is_string ( $name )) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Invalid body segment key ("' . gettype ( $name ) . '")' );
		}
		
		if ((null !== $parent) && ! is_string ( $parent )) {
			require_once 'Zend/Controller/Response/Exception.php';
			throw new Zend_Controller_Response_Exception ( 'Invalid body segment parent key ("' . gettype ( $parent ) . '")' );
		}
		
		if (isset ( $this->_body [$name] )) {
			unset ( $this->_body [$name] );
		}
		
		if ((null === $parent) || ! isset ( $this->_body [$parent] )) {
			return $this->append ( $name, $content );
		}
		
		$ins = array ($name => ( string ) $content );
		$keys = array_keys ( $this->_body );
		$loc = array_search ( $parent, $keys );
		if (! $before) {
			// Increment location if not inserting before
			++ $loc;
		}
		
		if (0 === $loc) {
			// If location of key is 0, we're prepending
			$this->_body = $ins + $this->_body;
		} elseif ($loc >= (count ( $this->_body ))) {
			// If location of key is maximal, we're appending
			$this->_body = $this->_body + $ins;
		} else {
			// Otherwise, insert at location specified
			$pre = array_slice ( $this->_body, 0, $loc );
			$post = array_slice ( $this->_body, $loc );
			$this->_body = $pre + $ins + $post;
		}
		
		return $this;
	}
	
	/**
	 * Echo the body segments
	 *
	 * @return void
	 */
	public function outputBody() {
		$body = implode ( '', $this->_body );
		echo $body;
	}
	
	/**
	 * Register an exception with the response
	 *
	 * @param Exception $e
	 * @return Zend_Controller_Response_Abstract
	 */
	public function setException(Exception $e) {
		$this->_exceptions [] = $e;
		return $this;
	}
	
	/**
	 * Retrieve the exception stack
	 *
	 * @return array
	 */
	public function getException() {
		return $this->_exceptions;
	}
	
	/**
	 * Has an exception been registered with the response?
	 *
	 * @return boolean
	 */
	public function isException() {
		return ! empty ( $this->_exceptions );
	}
	
	/**
	 * Does the response object contain an exception of a given type?
	 *
	 * @param  string $type
	 * @return boolean
	 */
	public function hasExceptionOfType($type) {
		foreach ( $this->_exceptions as $e ) {
			if ($e instanceof $type) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Does the response object contain an exception with a given message?
	 *
	 * @param  string $message
	 * @return boolean
	 */
	public function hasExceptionOfMessage($message) {
		foreach ( $this->_exceptions as $e ) {
			if ($message == $e->getMessage ()) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Does the response object contain an exception with a given code?
	 *
	 * @param  int $code
	 * @return boolean
	 */
	public function hasExceptionOfCode($code) {
		$code = ( int ) $code;
		foreach ( $this->_exceptions as $e ) {
			if ($code == $e->getCode ()) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Retrieve all exceptions of a given type
	 *
	 * @param  string $type
	 * @return false|array
	 */
	public function getExceptionByType($type) {
		$exceptions = array ();
		foreach ( $this->_exceptions as $e ) {
			if ($e instanceof $type) {
				$exceptions [] = $e;
			}
		}
		
		if (empty ( $exceptions )) {
			$exceptions = false;
		}
		
		return $exceptions;
	}
	
	/**
	 * Retrieve all exceptions of a given message
	 *
	 * @param  string $message
	 * @return false|array
	 */
	public function getExceptionByMessage($message) {
		$exceptions = array ();
		foreach ( $this->_exceptions as $e ) {
			if ($message == $e->getMessage ()) {
				$exceptions [] = $e;
			}
		}
		
		if (empty ( $exceptions )) {
			$exceptions = false;
		}
		
		return $exceptions;
	}
	
	/**
	 * Retrieve all exceptions of a given code
	 *
	 * @param mixed $code
	 * @return void
	 */
	public function getExceptionByCode($code) {
		$code = ( int ) $code;
		$exceptions = array ();
		foreach ( $this->_exceptions as $e ) {
			if ($code == $e->getCode ()) {
				$exceptions [] = $e;
			}
		}
		
		if (empty ( $exceptions )) {
			$exceptions = false;
		}
		
		return $exceptions;
	}
	
	/**
	 * Whether or not to render exceptions (off by default)
	 *
	 * If called with no arguments or a null argument, returns the value of the
	 * flag; otherwise, sets it and returns the current value.
	 *
	 * @param boolean $flag Optional
	 * @return boolean
	 */
	public function renderExceptions($flag = null) {
		if (null !== $flag) {
			$this->_renderExceptions = $flag ? true : false;
		}
		
		return $this->_renderExceptions;
	}
	
	/**
	 * Send the response, including all headers, rendering exceptions if so
	 * requested.
	 *
	 * @return void
	 */
	public function sendResponse() {
		$this->sendHeaders ();
		
		if ($this->isException () && $this->renderExceptions ()) {
			$exceptions = '';
			foreach ( $this->getException () as $e ) {
				$exceptions .= $e->__toString () . "\n";
			}
			echo $exceptions;
			return;
		}
		
		$this->outputBody ();
	}
	
	/**
	 * Magic __toString functionality
	 *
	 * Proxies to {@link sendResponse()} and returns response value as string
	 * using output buffering.
	 *
	 * @return string
	 */
	public function __toString() {
		ob_start ();
		$this->sendResponse ();
		return ob_get_clean ();
	}
}
